from django.db import models
from django.db.models import Min, Max
from django.core.exceptions import ObjectDoesNotExist

class Schedule(models.Model):
    """Store the sections in a Schedule."""
    sections = models.ManyToManyField('Section')

    def get_all_course_slots(self):
        """Get a list of all CourseSlots in this Schedule."""
        cslist = []
        for section in self.sections.all():
            for cs in section.course_slots.all():
                cslist.append(cs)
        return cslist

    def get_output_data(self):
        """Return timetable data. Basically, a list where
        [0] => [courseslots on monday],
        [1] => [courseslots on tuesday], etc.
        Meant to make generating an HTML schedule easy.
        """
        cslist = self.get_all_course_slots()
        data = []
        for i, daychoice in enumerate(CourseSlot.DAY_CHOICES):
            daycode, dayname = daychoice
            relevant_cslist = [cs for cs in cslist if cs.day_of_week == daycode]
            sort_comp = lambda c : c.begin_time
            sorted_relevant_cslist = sorted(relevant_cslist, key=sort_comp)
            data.append(sorted_relevant_cslist)
        return data

    def add_section(self, section):
        """Add passed section to the sections attribute
        returns false if a Time conflict arise, true otherwise.
        Deprecated."""
        for a_section in self.sections.all():
            if a_section.conflicts_with(section):
                return False
        self.sections.add(section)
        return True
        
    def remove_section(self, section):
        """Remove passed section from the sections attribute
        for some reason, remove does() not throw an exception if
        schedule empty.
        Deprecated."""
        self.sections.remove(section)

    def __str__(self):
        out = "Schedule #%d \n<br/>\n" % self.pk
        for sec in self.sections.all():
            out += str(sec) + "\n<br/>\n"
        return out
        
class Course(models.Model):
    """Store information about a Course that isn't temporal. (Temporal information stored
    in Course.sections."""
    name = models.CharField(max_length=300) # e.g. The Software Process
    number = models.IntegerField() # e.g. 341, 346, 248, etc.
    department = models.CharField(max_length=6) # e.g. COMP, SOEN
    sections = models.ManyToManyField('Section', related_name='course_sections')

    def __str__(self):
        return "%s %d %s" % (self.department, self.number, self.name)

class Section(models.Model):
    """Store information about a section of a course. A section is a unique combination
    of lecture, tutorial, and lab (if they exist). It has its own section/lab code."""
    # TA: different sections have different profs
    prof = models.CharField(max_length=300)

    course_slots = models.ManyToManyField('CourseSlot', related_name='section_courseslots')
    section_code = models.CharField(max_length=5)

    # TA: not using this until the data is finalized
    SEMESTER_CHOICES = (
        ('S', 'Summer'),
        ('S1', 'Summer1'),
        ('S2', 'Summer2'),
        ('F', 'Fall'),
        ('W', 'Winter'),
    )
    semester = models.CharField(choices=SEMESTER_CHOICES, max_length=5)

    def get_course(self):
        return self.course_sections.all()[0]

    def __str__(self):
        #c = self.objects.select_related('course')
        #c = Course.objects.filter(sections__contains=self)
        c = self.get_course()
        return str(c.name) + " Section code: %s" % self.section_code
        
    def conflicts_with(self, other_section):
        """Returns true if a time conflict arises between the
        calling section and the passed section.
        Deprecated."""
        for a_slot in self.course_slots.all():
            for other_slot in other_section.course_slots.all():
                if a_slot.conflicts_with(other_slot):
                    return True
        return False

class CourseSlot(models.Model):
    """Store the day and time of a meeting time (lecture/tutorial, etc.)"""
    DAY_CHOICES = (
        ('M', 'Monday'),
        ('T', 'Tuesday'),
        ('W', 'Wednesday'),
        ('H',  'Thursday'),
        ('F',  'Friday'),
    )
    day_of_week = models.CharField(max_length=1, choices=DAY_CHOICES)
    begin_time = models.TimeField()
    end_time = models.TimeField()
    slot_type = models.CharField(max_length=20) # type is a reserved python word

    def __str__(self):
        return "%s (%s - %s)" % (self.day_of_week,
                self.begin_time.strftime("%H:%M"),
                self.end_time.strftime("%H:%M"))

    def get_course(self):
        return self.get_section().course_sections.all()[0]

    def get_section(self):
        return self.section_courseslots.all()[0]

    #bug: can return multiple objects
    #TA: get() shouldn't return multiple objects...
    def get_semester(self):
        """Get the semester code that this CourseSlot is in. Needs to reference
        this CourseSlot's section."""
        return self.section_courseslots.filter()[0].semester

    def conflicts_with(self, other_slot):
        """Returns true if there is a time conflict between the calling slot and the passed slot."""
        if self.day_of_week != other_slot.day_of_week:
            conflict = False
        else:
            conflict = (self.begin_time <= other_slot.end_time and self.begin_time >= other_slot.begin_time) or\
                    (self.end_time <= other_slot.end_time and self.end_time >= other_slot.begin_time)
        return conflict

    def happens_within_bounds(self, minbound, maxbound):
        """Is the CourseSlot totally within the min and max bounds
        given?"""
        return (minbound <= self.begin_time and self.end_time <= maxbound)

class User(models.Model):
    """
    Binds a user id (used for identifying yourself in requests) with
    their MetaSchedule.
    """
    session_id = models.CharField(max_length=300)
    schedule = models.OneToOneField(Schedule, null=True) # why is this here?
    meta_schedule = models.OneToOneField('MetaSchedule')

    @staticmethod
    def authenticate(s_id):
        """Return the User with the given session key or
        return false if no User has that session key."""
        try:
           return (User.objects.get(session_id = s_id))
        except ObjectDoesNotExist:
            return False

class PreferencesFilter(models.Model):
    """
    PreferencesFilter stores the preferences of a user like time bounds
    on their classes. Ok, only time bounds on their classes.
    """
    earliest_class_start = models.TimeField()
    latest_class_end = models.TimeField()

class MetaSchedule(models.Model):
    """
    Store a set of courses and *all* the possible schedules generated
    from them
    """
    prefs = models.OneToOneField('PreferencesFilter', null=True)
    schedules = models.ManyToManyField('Schedule')
    courses = models.ManyToManyField('Course')

    def generate(self, permid, semester):
        """Generate the first valid schedule after permutation id <permid>."""
        import itertools
        def next_noconflict_schedule(iter, startat):
            """
            Get the <startat>th permutation of the courses in the meta_schedule,
            and if that permutation doesn't have conflicts, yield it. Else find
            the next permutation that doesn't have conflicts and yield that.
            Also yield the index of the permutation.
            The <iter> parameter is a generator that generates permutations of
            course sections. next_noconflict_schedule() is effectively a wrapper
            that filters out all the invalid schedules
            TODO modify to use PreferencesFilter
            Returns (i, n) where i is the index of the permutation and
            n is the found nonconflicting schedule.
            """
            def conflicts(courseslots):
                """
                Are there conflicts in <courseslots>?
                """
                sems = {}
                for cs in courseslots:
                    # Sort by semester and day of week
                    if not cs.happens_within_bounds(self.prefs.earliest_class_start, self.prefs.latest_class_end):
                        return True
                    cssem = cs.get_semester()
                    if cssem not in sems.keys():
                        sems[cssem] = {}
                    if cs.day_of_week not in sems[cssem].keys():
                        sems[cssem][cs.day_of_week] = []
                    sems[cssem][cs.day_of_week].append(cs)
                    dayar = sems[cssem][cs.day_of_week]
                    if len(dayar) > 1:
                        # go through all the courses that day
                        # and find any overlap
                        for possible_conflicter in dayar:
                            for possible_conflictee in dayar:
                                if possible_conflicter is possible_conflictee:
                                    continue
                                if possible_conflicter.conflicts_with(possible_conflictee):
                                    return True
                return False
            i = 0
            while True:
                n = iter.next()
                i += 1
                if i < startat:
                    continue
                # collect all the course_slots from all the sections
                # FILTER OUT THINGS NOT FROM THIS SEMESTER
                course_slot_lists = [s.course_slots for s in n if s.semester==semester]
                all_course_slots = []
                for l in course_slot_lists:
                    for css in l.all():
                        all_course_slots.append(css)

                # if there aren't any conflicts, yield the permid and
                # the schedule we found
                if not conflicts(all_course_slots):
                    yield (i, n)
                # if we haven't found anything, we try again with the next
                # permutation
        # end next_noconflict_schedule()

        # clear the schedules.
        # TODO: cache previously generated schedules.
        self.schedules.clear()
        schedules = []
        sections = []

        # get all sections from this semester
        for c in self.courses.all():
            secs = c.sections.filter(semester=semester)
            sections.append(secs)


        # this just creates a generator that generates permutations of sections
        iter = apply(itertools.product, [seclist for seclist in sections if len(seclist) > 0])

        iterator = next_noconflict_schedule(iter, permid)

        try:
            (iternum, the_one_we_want) = iterator.next()
        except StopIteration:
            return False

        # now that we know which sections we want, add them to a schedule
        # in our MetaSchedule
        s = Schedule()
        s.save()
        for i in the_one_we_want:
            s.sections.add(i)
            s.save()
            self.schedules.add(s)
            self.save()

        # return the ID of this schedule-permutation.
        # If the user wants another schedule, they should
        # begin searching at iternum + 1
        return iternum
